/*****************************************************************************
 * Copyright (c) 2014-2020 OpenRCT2 developers
 *
 * For a complete list of all authors, please refer to contributors.md
 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
 *
 * OpenRCT2 is licensed under the GNU General Public License version 3.
 *****************************************************************************/

#pragma once

#ifdef ENABLE_SCRIPTING

#    include "../actions/GameAction.h"
#    include "../interface/Screenshot.h"
#    include "../object/ObjectManager.h"
#    include "../scenario/Scenario.h"
#    include "Duktape.hpp"
#    include "HookEngine.h"
#    include "ScConfiguration.hpp"
#    include "ScDisposable.hpp"
#    include "ScObject.hpp"
#    include "ScriptEngine.h"

#    include <cstdio>
#    include <memory>

namespace OpenRCT2::Scripting
{
    class ScContext
    {
    private:
        ScriptExecutionInfo& _execInfo;
        HookEngine& _hookEngine;

    public:
        ScContext(ScriptExecutionInfo& execInfo, HookEngine& hookEngine)
            : _execInfo(execInfo)
            , _hookEngine(hookEngine)
        {
        }

    private:
        std::shared_ptr<ScConfiguration> configuration_get()
        {
            return std::make_shared<ScConfiguration>();
        }

        std::shared_ptr<ScConfiguration> sharedStorage_get()
        {
            auto& scriptEngine = GetContext()->GetScriptEngine();
            return std::make_shared<ScConfiguration>(scriptEngine.GetSharedStorage());
        }

        void captureImage(const DukValue& options)
        {
            auto ctx = GetContext()->GetScriptEngine().GetContext();
            try
            {
                CaptureOptions captureOptions;
                captureOptions.Filename = fs::u8path(AsOrDefault(options["filename"], ""));
                captureOptions.Rotation = options["rotation"].as_int() & 3;
                captureOptions.Zoom = ZoomLevel(options["zoom"].as_int());

                auto dukPosition = options["position"];
                if (dukPosition.type() == DukValue::Type::OBJECT)
                {
                    CaptureView view;
                    view.Width = options["width"].as_int();
                    view.Height = options["height"].as_int();
                    view.Position.x = dukPosition["x"].as_int();
                    view.Position.y = dukPosition["y"].as_int();
                    captureOptions.View = view;
                }

                CaptureImage(captureOptions);
            }
            catch (const DukException&)
            {
                duk_error(ctx, DUK_ERR_ERROR, "Invalid options.");
            }
            catch (const std::exception& ex)
            {
                duk_error(ctx, DUK_ERR_ERROR, ex.what());
            }
        }

        static DukValue CreateScObject(duk_context* ctx, uint8_t type, int32_t index)
        {
            switch (type)
            {
                case OBJECT_TYPE_RIDE:
                    return GetObjectAsDukValue(ctx, std::make_shared<ScRideObject>(type, index));
                case OBJECT_TYPE_SMALL_SCENERY:
                    return GetObjectAsDukValue(ctx, std::make_shared<ScSmallSceneryObject>(type, index));
                default:
                    return GetObjectAsDukValue(ctx, std::make_shared<ScObject>(type, index));
            }
        }

        DukValue getObject(const std::string& typez, int32_t index) const
        {
            auto ctx = GetContext()->GetScriptEngine().GetContext();
            auto& objManager = GetContext()->GetObjectManager();

            auto type = ScObject::StringToObjectType(typez);
            if (type)
            {
                auto obj = objManager.GetLoadedObject(*type, index);
                if (obj != nullptr)
                {
                    return CreateScObject(ctx, *type, index);
                }
            }
            else
            {
                duk_error(ctx, DUK_ERR_ERROR, "Invalid object type.");
            }
            return ToDuk(ctx, nullptr);
        }

        std::vector<DukValue> getAllObjects(const std::string& typez) const
        {
            auto ctx = GetContext()->GetScriptEngine().GetContext();
            auto& objManager = GetContext()->GetObjectManager();

            std::vector<DukValue> result;
            auto type = ScObject::StringToObjectType(typez);
            if (type)
            {
                auto count = object_entry_group_counts[*type];
                for (int32_t i = 0; i < count; i++)
                {
                    auto obj = objManager.GetLoadedObject(*type, i);
                    if (obj != nullptr)
                    {
                        result.push_back(CreateScObject(ctx, *type, i));
                    }
                }
            }
            else
            {
                duk_error(ctx, DUK_ERR_ERROR, "Invalid object type.");
            }
            return result;
        }

        int32_t getRandom(int32_t min, int32_t max)
        {
            ThrowIfGameStateNotMutable();
            if (min >= max)
                return min;
            int32_t range = max - min;
            return min + scenario_rand_max(range);
        }

        std::shared_ptr<ScDisposable> subscribe(const std::string& hook, const DukValue& callback)
        {
            auto hookType = GetHookType(hook);
            if (hookType == HOOK_TYPE::UNDEFINED)
            {
                throw DukException() << "Unknown hook type: " << hook;
            }

            if (!callback.is_function())
            {
                throw DukException() << "Expected function for callback";
            }

            auto owner = _execInfo.GetCurrentPlugin();
            if (owner == nullptr)
            {
                throw DukException() << "Not in a plugin context";
            }

            auto cookie = _hookEngine.Subscribe(hookType, owner, callback);
            return std::make_shared<ScDisposable>([this, hookType, cookie]() { _hookEngine.Unsubscribe(hookType, cookie); });
        }

        void queryAction(const std::string& action, const DukValue& args, const DukValue& callback)
        {
            QueryOrExecuteAction(action, args, callback, false);
        }

        void executeAction(const std::string& action, const DukValue& args, const DukValue& callback)
        {
            QueryOrExecuteAction(action, args, callback, true);
        }

        void QueryOrExecuteAction(const std::string& actionid, const DukValue& args, const DukValue& callback, bool isExecute)
        {
            auto& scriptEngine = GetContext()->GetScriptEngine();
            auto ctx = scriptEngine.GetContext();
            try
            {
                auto action = scriptEngine.CreateGameAction(actionid, args);
                if (action != nullptr)
                {
                    auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin();
                    if (isExecute)
                    {
                        action->SetCallback([this, plugin, callback](const GameAction*, const GameActionResult* res) -> void {
                            HandleGameActionResult(plugin, *res, callback);
                        });
                        GameActions::Execute(action.get());
                    }
                    else
                    {
                        auto res = GameActions::Query(action.get());
                        HandleGameActionResult(plugin, *res, callback);
                    }
                }
                else
                {
                    duk_error(ctx, DUK_ERR_ERROR, "Unknown action.");
                }
            }
            catch (DukException&)
            {
                duk_error(ctx, DUK_ERR_ERROR, "Invalid action parameters.");
            }
        }

        void HandleGameActionResult(
            const std::shared_ptr<Plugin>& plugin, const GameActionResult& res, const DukValue& callback)
        {
            // Construct result object
            auto& scriptEngine = GetContext()->GetScriptEngine();
            auto ctx = scriptEngine.GetContext();
            auto objIdx = duk_push_object(ctx);
            duk_push_int(ctx, static_cast<duk_int_t>(res.Error));
            duk_put_prop_string(ctx, objIdx, "error");

            if (res.Error != GA_ERROR::OK)
            {
                auto title = res.GetErrorTitle();
                duk_push_string(ctx, title.c_str());
                duk_put_prop_string(ctx, objIdx, "errorTitle");

                auto message = res.GetErrorMessage();
                duk_push_string(ctx, message.c_str());
                duk_put_prop_string(ctx, objIdx, "errorMessage");
            }

            duk_push_int(ctx, static_cast<duk_int_t>(res.Cost));
            duk_put_prop_string(ctx, objIdx, "cost");

            duk_push_int(ctx, static_cast<duk_int_t>(res.Expenditure));
            duk_put_prop_string(ctx, objIdx, "expenditureType");

            auto args = DukValue::take_from_stack(ctx);

            if (callback.is_function())
            {
                // Call the plugin callback and pass the result object
                scriptEngine.ExecutePluginCall(plugin, callback, { args }, false);
            }
        }

        void registerAction(const std::string& action, const DukValue& query, const DukValue& execute)
        {
            auto& scriptEngine = GetContext()->GetScriptEngine();
            auto plugin = scriptEngine.GetExecInfo().GetCurrentPlugin();
            auto ctx = scriptEngine.GetContext();
            if (!query.is_function())
            {
                duk_error(ctx, DUK_ERR_ERROR, "query was not a function.");
            }
            else if (!execute.is_function())
            {
                duk_error(ctx, DUK_ERR_ERROR, "execute was not a function.");
            }
            else if (!scriptEngine.RegisterCustomAction(plugin, action, query, execute))
            {
                duk_error(ctx, DUK_ERR_ERROR, "action has already been registered.");
            }
        }

    public:
        static void Register(duk_context* ctx)
        {
            dukglue_register_property(ctx, &ScContext::configuration_get, nullptr, "configuration");
            dukglue_register_property(ctx, &ScContext::sharedStorage_get, nullptr, "sharedStorage");
            dukglue_register_method(ctx, &ScContext::captureImage, "captureImage");
            dukglue_register_method(ctx, &ScContext::getObject, "getObject");
            dukglue_register_method(ctx, &ScContext::getAllObjects, "getAllObjects");
            dukglue_register_method(ctx, &ScContext::getRandom, "getRandom");
            dukglue_register_method(ctx, &ScContext::subscribe, "subscribe");
            dukglue_register_method(ctx, &ScContext::queryAction, "queryAction");
            dukglue_register_method(ctx, &ScContext::executeAction, "executeAction");
            dukglue_register_method(ctx, &ScContext::registerAction, "registerAction");
        }
    };
} // namespace OpenRCT2::Scripting

#endif
